package persistence.repository;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import persistence.PersistenceController;
import persistence.Persistent;
import util.DuplicationException;
import util.NotFoundException;
import domain.user.Member;
import domain.user.Privilege;
import domain.user.User;

/**
 * responsible for creation and destruction of users, whether they are members or not.
 * this class also enforces unique name for members.
 *
 * design: generally the repository only keeps members. but we still want users and members to share id
 * range. therefore, we create an object to inherit from Repository<Member>, use its persistence
 * abstraction facilities, but have its id-handling take non-member users into account as well. in
 * addition, this class is responsible for the create() method.
 *
 * to keep things simple, guests get a negative id.
 *
 * @author idan
 */
public class UserRepository extends Repository<Member>
{
	/**
	 * there is going to be very much code duplication now. this is due to java's limitation on
	 * inheritance.
	 */

	private final Map<Long, User> idsToUsers ;
	private ReentrantReadWriteLock lock ;
	private long currentGuestId ;

	/**
	 * @param persistence persistence provider to store objects.
	 * @pre 'persistence' persists objects of type 'Member'.
	 */
	public UserRepository(final PersistenceController persistence)
	{
		super(persistence) ;
		this.lock = new ReentrantReadWriteLock() ;
		this.idsToUsers = new HashMap<Long, User>() ;
		this.currentGuestId = 0 ;
	}

	/**
	 * @param id id to check for.
	 * @return 'true' if the repository contains a user whose id is 'id', 'false' otherwise.
	 */
	public boolean containsUser(final long id)
	{
		this.lock.readLock().lock() ;
		try
		{
			return this.idsToUsers.containsKey(id) || this.contains(id) ;
		}
		finally
		{
			this.lock.readLock().unlock() ;
		}
	}

	/**
	 * @param name name of the new member. must be unique.
	 * @param password password of the new member.
	 * @param privileges privileges of the new member.
	 * @return the newly created member.
	 * @throws DuplicationException if a member with the same name already exists.
	 * @post member exists in the repository.
	 */
	public Member create(final String name, final String password, final Set<Privilege> privileges)
		throws DuplicationException
	{
		if (this.getMemberByName(name) == null)
		{
			final Member member = new Member(Persistent.INVALID_ID, name, password, privileges) ;
			this.put(member) ;
			return member ;
		}
		else
		{
			throw new DuplicationException(name) ;
		}
	}

	/**
	 * @param privileges privileges of the new user.
	 * @return the newly created user.
	 * @post user exists on the server, but is not persistent.
	 */
	public User createUser(final Set<Privilege> privileges)
	{
		this.lock.writeLock().lock() ;
		try
		{
			final long id = this.getNewId() ;
			final User user = new User(id, privileges) ;
			this.idsToUsers.put(id, user) ;
			return user ;
		}
		finally
		{
			this.lock.writeLock().unlock() ;
		}

	}

	/**
	 * @see persistence.repository.Repository#destroy(long)
	 */
	@Override
	public void destroy(final long id) throws NotFoundException
	{
		if (this.idsToUsers.containsKey(id))
		{
			this.idsToUsers.remove(id) ;
		}
		else
		{
			super.destroy(id) ;
		}
	}

	/**
	 * get a whole set of users.
	 * @return set of users such that this.contains(uses.id). the set may be empty.
	 * @throws NotFoundException if one of the id's was not found.
	 */
	public Set<User> getAllUsers() throws NotFoundException
	{
		this.lock.readLock().lock() ;
		try
		{
			final Set<User> result = new HashSet<User>() ;
			result.addAll(this.idsToUsers.values()) ;
			result.addAll(super.getAll()) ;
			return result ;
		}
		finally
		{
			this.lock.readLock().unlock() ;
		}
	}

	/**
	 * @param name name of the member.
	 * @return the member whose name is 'name', or 'null' if no such member exists. the reason we dont
	 * throw an exception is that member names are external property, unlike id's which are internal
	 * property, and bad id is indeed exceptional.
	 */
	public Member getMemberByName(final String name)
	{
		final Collection<Member> members = this.getAll() ;
		for (Member m: members)
		{
			if (m.getUsername().equals(name))
			{
				return m ;
			}
		}
		return null ;
	}

	/**
	 * @param id id of the user to get.
	 * @return user associated with the given id.
	 * @throws NotFoundException if the repository does not contain a user with such id.
	 */
	public User getUser(final long id) throws NotFoundException
	{
		final User user ;
		this.lock.readLock().lock() ;
		try
		{
			user = this.idsToUsers.get(id) ;
			if (user == null)
			{
				return super.get(id) ;
			}
			else
			{
				return user ;
			}
		}
		finally
		{
			this.lock.readLock().unlock() ;
		}
	}

	private long getNewId()
	{
		this.currentGuestId-- ;
		return this.currentGuestId ;
	}
}